Skip to content

fix(codex): handle remote request_user_input approvals#382

Merged
hqhq1025 merged 1 commit intotiann:mainfrom
lwc-alex:codex/fix-remote-request-user-input
Apr 7, 2026
Merged

fix(codex): handle remote request_user_input approvals#382
hqhq1025 merged 1 commit intotiann:mainfrom
lwc-alex:codex/fix-remote-request-user-input

Conversation

@lwc-alex
Copy link
Copy Markdown
Contributor

@lwc-alex lwc-alex commented Apr 1, 2026

Fix remote Codex sessions hanging/canceling on request_user_input tool approvals.

Root cause:

  • codexRemoteLauncher registers app-server permission handlers without onUserInputRequest
  • appServerPermissionAdapter falls back to cancel for item/tool/requestUserInput

This patch wires request_user_input into the existing Codex remote permission flow and adds focused tests.

Validated with:

  • bun x vitest run src/codex/utils/permissionHandler.test.ts src/codex/utils/appServerPermissionAdapter.test.ts src/codex/codexRemoteLauncher.test.ts
  • bun run typecheck

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Explicit deny for request_user_input is downgraded to cancel. handleUserInputRequest() rejects the non-accept path, and this launcher catch always returns { decision: 'cancel' }, so Telegram or any generic denyPermission() caller can never propagate decline back to Codex. The same path also records the completed request as canceled instead of denied in cli/src/codex/utils/permissionHandler.ts. This changes user-visible behavior for deny actions instead of only fixing the hang. Evidence: cli/src/codex/codexRemoteLauncher.ts:519, cli/src/codex/utils/permissionHandler.ts:168, hub/src/telegram/callbacks.ts:90.
    Suggested fix:
    type UserInputApproval =
        | { decision: 'accept'; answers: RequestUserInputAnswers }
        | { decision: 'decline' | 'cancel'; reason?: string }
    
    const result = await permissionHandler.handleUserInputRequest(id, input)
    return result.decision === 'accept'
        ? result
        : { decision: result.decision }

Summary

  • Review mode: initial
  • 1 major finding. Main risk is that explicit deny paths for remote request_user_input prompts no longer behave like deny; they are collapsed into cancel/abort semantics instead.

Testing

  • Not run (automation environment missing bun; bun x vitest ... and bun run typecheck both failed with /bin/bash: bun: command not found)

HAPI Bot

const message = error instanceof Error ? error.message : String(error);
logger.debug(`[Codex] request_user_input failed: ${message}`);
return {
decision: 'cancel'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Explicit deny is being collapsed to cancel here. handleUserInputRequest() rejects every non-accept outcome, and this catch hard-codes { decision: 'cancel' }, so Telegram or any other denyPermission() caller never returns decline to Codex. The same path is then finalized as canceled instead of denied in cli/src/codex/utils/permissionHandler.ts:168, which changes the behavior of real deny actions rather than only fixing the hang.

Suggested fix:

type UserInputApproval =
    | { decision: 'accept'; answers: RequestUserInputAnswers }
    | { decision: 'decline' | 'cancel'; reason?: string }

const result = await permissionHandler.handleUserInputRequest(id, input)
return result.decision === 'accept'
    ? result
    : { decision: result.decision }

Copy link
Copy Markdown
Collaborator

@hqhq1025 hqhq1025 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — the root cause is real and the fix is correct. I traced the full approve flow end-to-end and it works.

Re: the bot finding about "deny downgraded to cancel" — this is consistent with existing behavior. All tool denials from Telegram (which calls denyPermission without a decision parameter) end up as cancel via mapDecision('abort'). The web UI's RequestUserInputFooter has no deny button, so this path is only reachable from Telegram. Not a regression.

Two minor suggestions (non-blocking):

1. Status label inversion in handlePermissionResponse

// Current (semantically inverted):
status: response.approved ? 'denied' : 'canceled',

// Should be:
status: response.approved ? 'canceled' : 'denied',

When approved: true but answers are empty, it's a system-level cancellation (not a user denial). When approved: false, it's a user denial. The current labels are swapped.

2. No onComplete on the deny path

The approve path calls onComplete (which sends tool-call-result to the web UI), but the deny path rejects without calling onComplete. This could leave the web UI tool card visually stuck in "pending" when denied from Telegram. Consider adding an onComplete call in the deny branch to match the regular tool pattern.

Both are minor — happy to see these addressed in a follow-up if you prefer.

@hqhq1025 hqhq1025 merged commit 56925f8 into tiann:main Apr 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants